package divestoclimb.checklist.data;

import java.util.Stack;

import divestoclimb.checklist.storage.DbAdapter;
import android.database.Cursor;

/**
 * A special list of Items which manipulates the items' internal order field
 * for use in the database.
 * The idea here is to reduce the need for reindexing the entire Item list in the
 * database any time the user inserts a new Item. If the database were using the
 * List's index to determine order, we'd have to reindex the list on every insert.
 * Instead, we use a separate field which by default gets a large increment, then
 * whenever a new item needs to be inserted between two other items we bisect the
 * gap in order between the two, and no existing records have to be updated. This
 * way reindexes only happen on collisions, when more than about
 * log2(STD_ORDER_INCREMENT) inserts happen in the same area.
 * @author Ben Roberts (divestoclimb@gmail.com)
 */
public class ItemList {
	
	public static final int STD_ORDER_INCREMENT=16;	// The number to increment
	// successive orders when appending
	// to and reindexing the list.

	protected Cursor mCursor;
	
	protected DbAdapter mDbHelper;
	
	public ItemList(Cursor c, DbAdapter dbHelper) {
		mCursor=c;
		mDbHelper=dbHelper;
	}
	
	/**
	 * Constructor that builds the backend Cursor for you.
	 * Be warned that you must manage this Cursor's lifecycle
	 * manually using getCursor().
	 * @param container_type
	 * @param container_id
	 * @param dbHelper
	 */
	public ItemList(int container_type, long container_id, DbAdapter dbHelper) {
		mDbHelper = dbHelper;
		mCursor = dbHelper.fetchItemsAsCursor(container_type, container_id);
	}
	
	public Cursor getCursor() { return mCursor; }
	public void setCursor(Cursor c) { mCursor=c; }

	/**
	 * Add Item i to the end of the list. If it already is in the list, it is
	 * moved to the end.
	 * @param i The Item to add or move
	 */
	public void add(Item i) {
		add(mCursor.getCount(), i);
	}

	/**
	 * Make Item i have position position in the ItemList. If i is already in
	 * this list, it is moved; otherwise it is added and the list length increases
	 * by one.
	 * @param position The new position for the Item
	 * @param i The Item to add or move
	 */
	public void add(int position, Item i) {
		Cursor c = mCursor;
		int order_prev, order, orderIndex = c.getColumnIndexOrThrow(DbAdapter.KEY_ITEM_ORDER);
		c.moveToPosition(position);
		
		// First we detect what the order of the item before us is.
		// If this is the beginning of the list, pretend there's an item with
		// order 0 before us. The cursor is already at the correct position to
		// detect the item after us.
		if(position == 0) {
			order_prev=0;
		} else {
			// If this Item isn't already in the list, its new neighbors are currently at position - 1 and 
			// position. However, if this item is already in the list and listed earlier than position,
			// it's effectively getting removed from the list and reinserted so although its neighbors will
			// end up at position - 1 and position, they are currently at position and position + 1. We take
			// that into account here by choosing where we start the operation.
			if(i.getOrder() == -1 || i.getOrder() > c.getInt(orderIndex)) {
				c.moveToPrevious();
			}
			order_prev = c.getInt(orderIndex);
			// Now we move the cursor to the next position so we can detect the order
			// of the next item.
			c.moveToNext();
		}

		if(c.getPosition() == c.getCount()) {
			// Trivial case. We're at the end of the list.
			i.setOrder(order_prev+STD_ORDER_INCREMENT);
			i.save(mDbHelper);
		} else {
			// Now we have to detect the order of the item after us.
			int order_next=c.getInt(orderIndex);

			// Now see if we can find an order in between order_last and order_next
			order=(order_next + order_prev)/2;

			if(order_next == i.getOrder()) {
				// We've been asked to add an item that already exists in this position.
				// Therefore we have nothing to do.
				return;
			} else if(order < order_next && order > order_prev) {
				// Normal scenario. Move the item.
				i.setOrder(order);
				i.save(mDbHelper);
			} else {
				// Collision, too many inserts. We have to reindex the rest of the
				// list.
				
				// Start with what i's new order will be.
				int new_order=order_prev+STD_ORDER_INCREMENT;

				i.setOrder(order_prev+STD_ORDER_INCREMENT);
				i.save(mDbHelper);

				// We have to update all Items at once or things could get screwed
				// up. So keep an array of all Items as we reindex, then save them
				// all once we get the orders right.
				Stack<Item> s = new Stack<Item>();
				s.push(i);
				do {
					// Retrieve the Item at this index
					Item it = DbAdapter.fetchItemFromCursor(c);
					// If we encounter the original item we're "adding" it's because
					// this item is really being moved. Skip it for now.
					if(it.equals(i)) { continue; }

					new_order+=STD_ORDER_INCREMENT;

					if(it.getOrder() >= new_order) {
						// This indicates the rest of the list is fine. There's
						// no need to continue reindexing.
						break;
					}

					// Update the order of this item
					it.setOrder(new_order);
					s.push(it);
				} while(c.moveToNext());

				// Now save everything we changed
				do {
					s.pop().save(mDbHelper);
				} while(! s.empty());
			}
		}
		// Re-query the cursor to update the list.
		c.requery();
	}
	
	/**
	 * Insert an Item above another item in the list with the
	 * given ID. If no Item in the list exists with the given
	 * ID, the Item is inserted at the end.
	 * @param id The ID above which to place the Item
	 * @param i The Item itself
	 */
	public void insertAbove(long id, Item i) {
		Cursor c = mCursor;
		boolean found=false;
		if(c.getCount() > 0) {
			c.moveToFirst();
			do {
				if(c.getLong(c.getColumnIndexOrThrow(DbAdapter.KEY_ITEM_ID)) == id) {
					found=true;
					break;
				}
			} while(c.moveToNext());
		}

		if(found) {
			add(c.getPosition(), i);
		} else {
			add(i);
		}
	}
}
